﻿/*
Copyright (c) 2009 Vratislav Cermak (vratacermak@gmail.com)
 
This file is part of TeachMe Project. See http://code.google.com/p/teach-me for more information.
 
TeachMe is an open source project. Using of TeachMe is free of charge.
It is distributed under GNU General Public License version 3 (GPLv3).
GPLv3 can be found at http://www.gnu.org/licenses/gpl.html.
 
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the license for more details.
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using TeachMe.Application.Configuration;
using TeachMe.Application.Events;
using TeachMe.Application.Exceptions;
using TeachMe.Application.Properties;

namespace TeachMe.Application.Data
{
    public class Dictionary
    {
        public event EventHandler<DictionaryEntriesEventArgs> EntriesModified;
        public event EventHandler<DictionaryEventArgs> UserSettingsModified;

        private readonly DictionaryUserData _userData;
        private readonly PrioritiesStore _prioritiesStore;

        public static Dictionary Create(NewDictionaryData newDictionaryData)
        {
            XmlEditor editor;

            try
            {
                editor = XmlEditor.Create(newDictionaryData.UserData.Path, "Dictionary");
            }
            catch (XmlEditorException e)
            {
                throw new DictionaryException("Dictionary could not be created.", e);
            }

            editor.SetAttributeValue("id", Guid.NewGuid().ToString());
            editor.SetAttributeValue("from", newDictionaryData.From);
            editor.SetAttributeValue("to", newDictionaryData.To);
            editor.SetAttributeValue("name", newDictionaryData.Name);
            editor.SetAttributeValue("version", Entries.Version.ToString());

            if (!String.IsNullOrEmpty(newDictionaryData.Description))
            {
                editor.SetAttributeValue("description", newDictionaryData.Description);
            }

            using (editor.AddNode("Entries"))
            {
                //
            }

            SaveXml(editor);

            return new Dictionary(newDictionaryData.UserData, newDictionaryData.PrioritiesStore);
        }

        public Dictionary(DictionaryUserData dictionaryUserData, PrioritiesStore prioritiesStore)
        {
            _userData = dictionaryUserData;
            _prioritiesStore = prioritiesStore;

            Load();
        }

        public void Load()
        {
            LoadEntries(_userData.Path);
            LoadPriorities(_prioritiesStore);
        }

        public DictionaryUserData UserData
        {
            get { return _userData; }
        }

        public Entries Entries { get; private set; }

        public Priorities Priorities { get; private set; }

        public override string ToString()
        {
            return Entries.Name;
        }

        public void UpdateEntry(EditedEntryDescriptor descriptor)
        {
            UpdateEntries(new List<EditedEntryDescriptor> { descriptor });
        }

        public void UpdateEntries(List<EditedEntryDescriptor> editedEntryDescriptors)
        {
            Logging.Debug(GetType(), "Updating '{0}' entries.", editedEntryDescriptors.Count);

            // sort out entries

            Dictionary<int, EditedEntryDescriptor> toUpdate = new Dictionary<int, EditedEntryDescriptor>();
            List<int> toDelete = new List<int>();
            List<EditedEntryDescriptor> toAdd = new List<EditedEntryDescriptor>();
            Dictionary<EditedEntryDescriptor, int> newIds = new Dictionary<EditedEntryDescriptor, int>();

            foreach (EditedEntryDescriptor editedEntryDescriptor in editedEntryDescriptors)
            {
                switch (editedEntryDescriptor.State)
                {
                    case EditedItemState.Normal:
                        if (editedEntryDescriptor.IsModified(EditedDescriptorValueType.From) || editedEntryDescriptor.IsModified(EditedDescriptorValueType.To) || editedEntryDescriptor.IsModified(EditedDescriptorValueType.Example))
                        {
                            toUpdate.Add(editedEntryDescriptor.Id, editedEntryDescriptor);
                        }
                        break;
                    case EditedItemState.Deleted:
                        toDelete.Add(editedEntryDescriptor.Id);
                        break;
                    case EditedItemState.New:
                        toAdd.Add(editedEntryDescriptor);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            // do entries

            if (toUpdate.Count != 0 || toAdd.Count != 0 || toDelete.Count != 0)
            {
                XmlEditor editor = LoadXml(UserData.Path);

                using (editor.LoadNode("Entries"))
                {
                    if (toAdd.Count != 0)
                    {
                        // entries must be added first, otherwise newly generated ids might be same as ids of
                        // just deleted entries

                        foreach (EditedEntryDescriptor editedEntryDescriptor in toAdd)
                        {
                            newIds.Add(editedEntryDescriptor, AddEntry(editor, editedEntryDescriptor));
                        }
                    }

                    if (toUpdate.Count != 0 || toDelete.Count != 0)
                    {
                        foreach (IDisposable entryNode in editor.LoadChildNodes())
                        {
                            using (entryNode)
                            {
                                int id = Int32.Parse(editor.GetAttributeValue("id"));

                                if (toUpdate.ContainsKey(id))
                                {
                                    UpdateEntry(editor, toUpdate[id]);
                                }
                                else if (toDelete.Contains(id))
                                {
                                    RemoveEntry(editor, id);
                                }
                            }
                        }
                    }
                }

                SaveXml(editor);
            }

            // sort out priorities

            toAdd.Clear();
            toDelete.Clear();
            toUpdate.Clear();

            foreach (EditedEntryDescriptor editedEntryDescriptor in editedEntryDescriptors)
            {
                if (editedEntryDescriptor.IsModified(EditedDescriptorValueType.Priority) || editedEntryDescriptor.State != EditedItemState.Normal)
                {
                    switch (editedEntryDescriptor.State)
                    {
                        case EditedItemState.Normal:
                            if (editedEntryDescriptor.Priority == Priority.Normal)
                            {
                                if (Priorities.Items.ContainsKey(editedEntryDescriptor.Id))
                                {
                                    toDelete.Add(editedEntryDescriptor.Id);
                                }
                            }
                            else
                            {
                                if (Priorities.Items.ContainsKey(editedEntryDescriptor.Id))
                                {
                                    toUpdate.Add(editedEntryDescriptor.Id, editedEntryDescriptor);
                                }
                                else
                                {
                                    toAdd.Add(editedEntryDescriptor);
                                }
                            }
                            break;
                        case EditedItemState.Deleted:
                            if (Priorities.Items.ContainsKey(editedEntryDescriptor.Id))
                            {
                                toDelete.Add(editedEntryDescriptor.Id);
                            }
                            break;
                        case EditedItemState.New:
                            if (editedEntryDescriptor.Priority != Priority.Normal)
                            {
                                toAdd.Add(editedEntryDescriptor);
                            }
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }
            }

            // do priorities

            if (toUpdate.Count != 0 || toAdd.Count != 0 || toDelete.Count != 0)
            {
                XmlEditor editor = LoadXml(Priorities.Path);

                using (editor.LoadNode("Entries"))
                {
                    foreach (IDisposable entryNode in editor.LoadChildNodes())
                    {
                        using (entryNode)
                        {
                            int id = Int32.Parse(editor.GetAttributeValue("id"));

                            if (toUpdate.ContainsKey(id))
                            {
                                using (editor.LoadNode("Priority"))
                                {
                                    editor.SetTextValue(toUpdate[id].Priority.ToString());
                                }

                                Priorities.Items[id] = toUpdate[id].Priority;
                            }
                            else if (toDelete.Contains(id))
                            {
                                editor.RemoveNode();

                                Priorities.Items.Remove(id);
                            }
                        }
                    }

                    foreach (EditedEntryDescriptor descriptor in toAdd)
                    {
                        int id = newIds.ContainsKey(descriptor) ? newIds[descriptor] : descriptor.Id;

                        using (editor.AddNode("Entry"))
                        {
                            editor.SetAttributeValue("id", id.ToString());

                            using (editor.AddNode("Priority"))
                            {
                                editor.SetTextValue(descriptor.Priority.ToString());
                            }
                        }

                        Priorities.Items.Add(id, descriptor.Priority);
                    }
                }

                editor.Save();
            }

            // fire event

            DictionaryEntriesEventArgs eventArgs = new DictionaryEntriesEventArgs { Dictionary = this };

            foreach (EditedEntryDescriptor descriptor in editedEntryDescriptors)
            {
                switch (descriptor.State)
                {
                    case EditedItemState.Normal:
                        eventArgs.Entries.Add(descriptor.Id, EditedItemState.Normal);
                        break;
                    case EditedItemState.Deleted:
                        eventArgs.Entries.Add(descriptor.Id, EditedItemState.Deleted);
                        break;
                    case EditedItemState.New:
                        eventArgs.Entries.Add(newIds[descriptor], EditedItemState.New);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            if (editedEntryDescriptors.Count > 0)
            {
                FireEntriesModified(eventArgs);
            }
        }

        public Priority GetEntryPriority(int entryId)
        {
            if (Priorities.Items.ContainsKey(entryId))
            {
                return Priorities.Items[entryId];
            }

            return Priority.Normal;
        }

        public void SetEntryPriority(int entryId, Priority priority)
        {
            EditedEntryDescriptor descriptor = EditedEntryDescriptor.Create(Entries.Items[entryId], GetEntryPriority(entryId));
            descriptor.Priority = priority;

            UpdateEntry(descriptor);
        }

        #region Priorities manipulation

        private void LoadPriorities(PrioritiesStore prioritiesStore)
        {
            if (!prioritiesStore.Priorities.ContainsKey(Entries.Id))
            {
                prioritiesStore.Create(Utilities.GetRandomFileName(_prioritiesStore.DirName, ".dus"), Entries.Id);
            }

            string path = prioritiesStore.Priorities[Entries.Id];

            Logging.Debug(GetType(), "Loading priorities from '{0}'.", path);

            XmlEditor editor = LoadXml(path);

            Guid dictionaryId = new Guid(editor.GetAttributeValue("dictionaryId"));

            Priorities = new Priorities(path, dictionaryId);

            bool saveNeeded = false;

            using (editor.LoadNode("Entries"))
            {
                foreach (IDisposable node in editor.LoadChildNodes())
                {
                    using (node)
                    {
                        int id = Int32.Parse(editor.GetAttributeValue("id"));

                        if (!Entries.Items.ContainsKey(id))
                        {
                            // priorities are not synchronized with dictionary, fix it

                            Logging.Warn(GetType(), "Priority refers to non existing entry id '{0}' and will be deleted.", id);

                            editor.RemoveNode();

                            saveNeeded = true;
                        }
                        else
                        {
                            using (editor.LoadNode("Priority"))
                            {
                                Priorities.Items.Add(id, editor.GetEnumValue<Priority>());
                            }
                        }
                    }
                }
            }

            if (saveNeeded)
            {
                SaveXml(editor);
            }
        }

        #endregion

        #region Entries manipulation

        private static XmlEditor LoadXml(string path)
        {
            Logging.Debug(typeof(Dictionary), "Loading xml '{0}'.", path);

            if (!File.Exists(path))
            {
                Logging.Error(typeof(Dictionary), "File '{0}' does not exist.", path);

                throw new DictionaryException(String.Format("File '{0}' does not exist.", path));
            }

            try
            {
                return XmlEditor.Load(path);
            }
            catch (Exception e)
            {
                Logging.Error(typeof(Dictionary), "Xml '{0}' could not be loaded.", path);

                throw new DictionaryException(String.Format("Xml '{0}' could not be loaded.", path), e);
            }
        }

        private void LoadEntries(string path)
        {
            Logging.Debug(GetType(), "Loading entries from '{0}'.", path);

            XmlEditor editor = LoadXml(path);

            if (!editor.CheckVersion(Entries.Version))
            {
                throw new DictionaryException("Invalid dictionary version.");
            }

            Guid id = new Guid(editor.GetAttributeValue("id"));
            string from = editor.GetAttributeValue("from");
            string to = editor.GetAttributeValue("to");
            string name = editor.GetAttributeValue("name");

            string description = null;
            if (editor.HasAttributeValue("description"))
            {
                description = editor.GetAttributeValue("description");
            }

            Entries = new Entries(id, name, description, from, to);

            using (editor.LoadNode("Entries"))
            {
                foreach (IDisposable entryNode in editor.LoadChildNodes())
                {
                    using (entryNode)
                    {
                        string entryFrom = null, entryTo = null, entryExample = null;
                        int entryId = Int32.Parse(editor.GetAttributeValue("id"));

                        if (editor.HasChildNode("From"))
                        {
                            using (editor.LoadNode("From"))
                            {
                                entryFrom = editor.GetTextValue();
                            }
                        }

                        if (editor.HasChildNode("To"))
                        {
                            using (editor.LoadNode("To"))
                            {
                                entryTo = editor.GetTextValue();
                            }
                        }

                        if (editor.HasChildNode("Example"))
                        {
                            using (editor.LoadNode("Example"))
                            {
                                entryExample = editor.GetTextValue();
                            }
                        }

                        Entries.Items.Add(entryId, new Entry(entryId, entryFrom, entryTo, entryExample));
                    }
                }
            }
        }

        private void RemoveEntry(XmlEditor editor, int id)
        {
            // data

            Entries.Items.Remove(id);

            // xml

            editor.RemoveNode();
        }

        private int AddEntry(XmlEditor editor, EditedEntryDescriptor descriptor)
        {
            // data

            int id = GenerateNewEntryId();

            Entry entry = new Entry(id, descriptor.From, descriptor.To, descriptor.Example);

            Entries.Items.Add(id, entry);

            // xml

            using (editor.AddNode("Entry"))
            {
                editor.SetAttributeValue("id", entry.Id.ToString());

                if (!String.IsNullOrEmpty(descriptor.From))
                {
                    using (editor.AddNode("From"))
                    {
                        editor.SetTextValue(descriptor.From);
                    }
                }

                if (!String.IsNullOrEmpty(descriptor.To))
                {
                    using (editor.AddNode("To"))
                    {
                        editor.SetTextValue(descriptor.To);
                    }
                }

                if (!String.IsNullOrEmpty(descriptor.Example))
                {
                    using (editor.AddNode("Example"))
                    {
                        editor.SetTextValue(descriptor.Example);
                    }
                }
            }

            return id;
        }

        private void UpdateEntry(XmlEditor editor, EditedEntryDescriptor descriptor)
        {
            // data

            Entries.Items[descriptor.Id].From = descriptor.From;
            Entries.Items[descriptor.Id].To = descriptor.To;
            Entries.Items[descriptor.Id].Example = descriptor.Example;

            // xml

            if (!String.IsNullOrEmpty(descriptor.From))
            {
                using (editor.LoadNode("From", true))
                {
                    editor.SetTextValue(descriptor.From);
                }
            }
            else
            {
                if (editor.HasChildNode("From"))
                {
                    editor.RemoveChildNode("From");
                }
            }

            if (!String.IsNullOrEmpty(descriptor.To))
            {
                using (editor.LoadNode("To", true))
                {
                    editor.SetTextValue(descriptor.To);
                }
            }
            else
            {
                if (editor.HasChildNode("To"))
                {
                    editor.RemoveChildNode("To");
                }
            }

            if (!String.IsNullOrEmpty(descriptor.Example))
            {
                using (editor.LoadNode("Example", true))
                {
                    editor.SetTextValue(descriptor.Example);
                }
            }
            else
            {
                if (editor.HasChildNode("Example"))
                {
                    editor.RemoveChildNode("Example");
                }
            }
        }

        private int GenerateNewEntryId()
        {
            int id = Entries.Items.Count;

            while (Entries.Items.ContainsKey(id))
            {
                id++;
            }

            return id;
        }

        #endregion

        public void UpdateMetadata(string name, string description, string from, string to)
        {
            // xml

            XmlEditor editor = LoadXml(_userData.Path);

            editor.SetAttributeValue("name", name);
            editor.SetAttributeValue("description", description);
            editor.SetAttributeValue("from", from);
            editor.SetAttributeValue("to", to);

            SaveXml(editor);

            // data

            Entries.Name = name;
            Entries.Description = description;
            Entries.From = from;
            Entries.To = to;
        }

        private static void SaveXml(XmlEditor editor)
        {
            try
            {
                editor.Save();
            }
            catch (XmlEditorException exception)
            {
                Logging.Error(typeof(Dictionary), exception, "Xml could not be saved");

                throw new DictionaryException("Xml could not be saved.", exception);
            }
        }

        public void UpdateUserSettings(bool enabled, EntryTextDirection entryTextDirection, Priority priority)
        {
            _userData.Enabled = enabled;
            _userData.EntryTextDirection = entryTextDirection;
            _userData.Priority = priority;

            FireUserSettingsModified(new DictionaryEventArgs { Dictionary = this });
        }

        private void FireEntriesModified(DictionaryEntriesEventArgs eventArgs)
        {
            EventHandler<DictionaryEntriesEventArgs> handler = EntriesModified;
            if (handler != null)
            {
                handler(this, eventArgs);
            }
        }

        private void FireUserSettingsModified(DictionaryEventArgs eventArgs)
        {
            EventHandler<DictionaryEventArgs> handler = UserSettingsModified;
            if (handler != null)
            {
                handler(this, eventArgs);
            }
        }
    }
}
